ATOM Documentation

← Back to App

Tenant Extraction Refactoring Guide

Overview

This guide shows how to refactor existing API routes to use the centralized getTenantFromRequest utility from src/lib/tenant/tenant-extractor.ts.

Before: Manual Tenant Extraction (Old Pattern)

// ❌ OLD: Manual tenant extraction
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getDatabase } from '@/lib/database'

export async function GET(req: NextRequest) {
    const session = await getServerSession(authOptions)
    if (!session) {
        return new NextResponse('Unauthorized', { status: 401 })
    }

    const tenantId = (session.user as any).tenant_id
    const db = getDatabase()

    // Manual query - error-prone, inconsistent
    const result = await db.query(`
        SELECT id, name FROM agents
        WHERE tenant_id = $1 OR tenant_id IS NULL
    `, [tenantId])

    return NextResponse.json(result.rows)
}

After: Centralized Tenant Extraction (New Pattern)

// ✅ NEW: Centralized tenant extraction
import { NextRequest, NextResponse } from 'next/server'
import { getTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'

export async function GET(req: NextRequest) {
    // Single function call handles all extraction methods
    const tenant = await getTenantFromRequest(req)

    if (!tenant) {
        return NextResponse.json(
            { error: 'Tenant not found' },
            { status: 404 }
        )
    }

    const db = getDatabase()

    // Clean query with proper tenant isolation
    const result = await db.query(`
        SELECT id, name FROM agents
        WHERE tenant_id = $1
    `, [tenant.id])

    return NextResponse.json(result.rows)
}

With Required Tenant (Throws on Missing)

import { NextRequest, NextResponse } from 'next/server'
import { requireTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'

export async function POST(req: NextRequest) {
    try {
        // Throws automatically if tenant not found
        const tenant = await requireTenantFromRequest(req)

        const db = getDatabase()
        const body = await req.json()

        // ... rest of implementation

    } catch (error: any) {
        if (error.message.includes('Unauthorized')) {
            return NextResponse.json(
                { error: 'Unauthorized' },
                { status: 401 }
            )
        }

        if (error.message.includes('Tenant not found')) {
            return NextResponse.json(
                { error: 'Tenant not found' },
                { status: 404 }
            )
        }

        // Other errors
        return NextResponse.json(
            { error: 'Internal server error' },
            { status: 500 }
        )
    }
}

Complete Refactoring Example: Agents Route

Before (`src/app/api/agents/route.ts`)

import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getDatabase } from '@/lib/database'
import { TenantService } from '@/lib/tenant/tenant-service'
import { EmailService } from '@/lib/email'

export async function GET(req: NextRequest) {
    try {
        const session = await getServerSession(authOptions)
        if (!session) {
            return new NextResponse('Unauthorized', { status: 401 })
        }

        const tenantId = (session.user as any).tenant_id
        const db = getDatabase()

        const result = await db.query(`
            SELECT id, name, role FROM agent_registry
            WHERE tenant_id = $1 OR tenant_id IS NULL
        `, [tenantId])

        return NextResponse.json(result.rows)
    } catch (error) {
        console.error('Failed to list agents:', error)
        return new NextResponse('Internal Error', { status: 500 })
    }
}

After (Refactored)

import { NextRequest, NextResponse } from 'next/server'
import { getTenantFromRequest } from '@/lib/tenant/tenant-extractor'
import { getDatabase } from '@/lib/database'

export async function GET(req: NextRequest) {
    try {
        // Centralized tenant extraction
        const tenant = await getTenantFromRequest(req)

        if (!tenant) {
            return NextResponse.json(
                { error: 'Tenant not found' },
                { status: 404 }
            )
        }

        const db = getDatabase()

        const result = await db.query(`
            SELECT id, name, role FROM agent_registry
            WHERE tenant_id = $1
        `, [tenant.id])

        return NextResponse.json(result.rows)
    } catch (error) {
        console.error('Failed to list agents:', error)
        return NextResponse.json(
            { error: 'Internal server error' },
            { status: 500 }
        )
    }
}

Benefits

1. **Consistency**

All routes use the same tenant extraction logic, reducing bugs.

2. **Multiple Extraction Methods**

Automatically tries multiple methods:

  • Session (authenticated users)
  • X-Tenant-ID header (internal calls)
  • Subdomain (tenant.atom.ai)
  • Custom domain (company.com)

3. **Proper Error Handling**

Centralized error handling with clear messages.

4. **Type Safety**

Full TypeScript support with TenantContext interface.

5. **Tenant Isolation**

No more OR tenant_id IS NULL bugs - always enforces tenant scoping.

Search & Replace Patterns

Find Manual Patterns to Replace

# Find manual session + tenant extraction
grep -r "getServerSession" src/app/api --include="*.ts"
grep -r "tenant_id = session" src/app/api --include="*.ts"
grep -r "tenant_id = (session.user" src/app/api --include="*.ts"

# Find direct database tenant queries
grep -r "SELECT.*tenant_id.*WHERE.*user_id" src/app/api --include="*.ts"

Replacement Pattern

// BEFORE:
const session = await getServerSession(authOptions)
if (!session) {
    return new NextResponse('Unauthorized', { status: 401 })
}
const tenantId = (session.user as any).tenant_id

// AFTER:
const tenant = await getTenantFromRequest(req)
if (!tenant) {
    return NextResponse.json({ error: 'Tenant not found' }, { status: 404 })
}

Testing

After refactoring, test these scenarios:

  1. **Authenticated user**: Tenant from session
  2. **Subdomain access**: tenant.atom.ai -> subdomain extraction
  3. **Custom domain**: company.com -> custom domain lookup
  4. **Internal API**: X-Tenant-ID header
  5. **Missing tenant**: Proper 404 error
  6. **Unauthenticated**: Proper 401 error

Migration Checklist

  • [ ] Update src/app/api/agents/route.ts
  • [ ] Update src/app/api/chat/route.ts
  • [ ] Update src/app/api/sessions/route.ts
  • [ ] Update all integration routes
  • [ ] Update workflow endpoints
  • [ ] Run E2E tests
  • [ ] Test tenant isolation
  • [ ] Verify subdomain routing
  • [ ] Test custom domain support

Files to Update

Based on the grep search, these files need refactoring:

  1. src/app/api/desktop/auth/route.ts
  2. src/app/api/agents/[id]/run/route.ts
  3. src/app/api/chat/route.ts
  4. Plus any other route with manual tenant extraction
  • src/lib/tenant/tenant-extractor.ts - Centralized extraction logic
  • backend-saas/core/auth.py - Backend equivalent (get_current_tenant)
  • src/lib/tenant/tenant-service.ts - Tenant business logic